package common.gateway.interphaze;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

import core.ITransfersQueue;
import core.InMemoryTransferQueue;
import core.domain.DataFile;
import core.domain.IOUtil;
import core.domain.Part;
import core.domain.Part.PartStatus;
import core.domain.PeerDesciptor;
import core.domain.Torrent;
import core.domain.messages.AbstractMessage;
import core.domain.messages.AvailabilityMessage;
import core.domain.messages.DisconnectionMessage;
import core.domain.messages.GetFilesMessage;
import core.domain.messages.GetPartMessage;
import core.domain.messages.GetPeersForTorrentMessage;
import core.domain.messages.NewFileMessage;
import core.domain.messages.PingPongMessage;
import core.domain.messages.PingPongMessage.PayLoad;
import core.domain.messages.StopServerMessage;
import core.domain.messages.AbstractMessage.MessageType;
import core.domain.messages.RegisterMessage;

public class PeerGateway implements IGateWay {

	private int trackerPort = 13468;
	private String trackerAddress = "localhost";
	private Socket trackerSocket;

	private PeerDesciptor selfDescriptor;

	private ServerSocket responderSocket;
	private int port;

	private ITransfersQueue transferQueue;
	private static PeerGateway instance;

	public static PeerGateway getInstance(int i) {
		if (instance == null) {
			instance = new PeerGateway(i);
		}
		return instance;
	}

	private Socket getTrackerSocket() {
		if (!trackerSocket.isConnected() || trackerSocket.isClosed()) {
			try {
				trackerSocket = new Socket(trackerAddress, trackerPort);
			} catch (UnknownHostException e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			} catch (IOException e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			}
		}
		return trackerSocket;
	}

	private PeerGateway(int i) {
		try {
			port = i;
			responderSocket = new ServerSocket(port);
			selfDescriptor = new PeerDesciptor(responderSocket.getInetAddress().getAddress(), responderSocket.getLocalPort());
			transferQueue = InMemoryTransferQueue.getInstance();
			trackerSocket = new Socket(trackerAddress, trackerPort);
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	@Override
	public void acceptConnections() {
		ping();
		while (true) {
			// handles incoming request
			handleRequests(responderSocket);

		}
	}

	private void ping() {
		// availability covers most of this... ...and more!
		//		- initiate ping requests to known hosts!
	}

	@Override
	public void handleRequests(ServerSocket serverSocket) {
		try {
			new Thread(new PeerIncomingRequestHandler(serverSocket.accept())).start();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	@Override
	public void closeDown() {
	}

	private class PeerIncomingRequestHandler implements Runnable {

		private Socket clientSocket;

		public PeerIncomingRequestHandler(Socket clientSocket) {
			this.clientSocket = clientSocket;
		}

		@Override
		public void run() {
			try {
				String messageFromSocket = IOUtil.readMessageFromSocket(clientSocket, false);
				AbstractMessage unmarshalledObject = IOUtil.getInstance(MessageType.matchType(messageFromSocket)).stringToObject(messageFromSocket);
				System.out.println(clientSocket + " connected, with message type: " + unmarshalledObject.getMessageType());
				// TODO: Beautify code with exctract to methods, or Helper classes!

				if (unmarshalledObject instanceof AvailabilityMessage) {
					availabilityHandler(unmarshalledObject, clientSocket);
				} else if (unmarshalledObject instanceof GetPartMessage) {
					getpartHandler(unmarshalledObject, clientSocket);
				} else if (unmarshalledObject instanceof NewFileMessage) {
					newfileHandler(unmarshalledObject, clientSocket);
				} else if (unmarshalledObject instanceof GetFilesMessage) {
					listOfFilesHandler(unmarshalledObject, clientSocket); // client ui sent this, dispatch a round trip
				} else if (unmarshalledObject instanceof GetPeersForTorrentMessage) {
					getpeersfortorrentHandler(unmarshalledObject, clientSocket);
				} else if (unmarshalledObject instanceof StopServerMessage) {
					stopserverHandler();
				} else if (unmarshalledObject instanceof PingPongMessage) {
					PingPongMessage pongMessage = (PingPongMessage) unmarshalledObject;
					pongMessage.setPayload(PayLoad.PONG);
					String pongReplyString = IOUtil.getInstance(PingPongMessage.class).objectToString(pongMessage);
					IOUtil.writeMessageToSocket(clientSocket, pongReplyString, true);
				}

			} catch (Exception e) {
				e.printStackTrace();
				throw new RuntimeException("problem in handling the peers incoming requests!", e);
			}

		}

		private void stopserverHandler() {
			//Unregister @ tracker, no further connection is handled.
			//No serversocket killing! letting ongoer leeches suck.
			//TODO: kill DisconnectionMessage.class from places where it was to use wire termination!
			String unregisterRequest = IOUtil.getInstance(DisconnectionMessage.class).objectToString(new DisconnectionMessage(null));
			IOUtil.writeMessageToSocket(getTrackerSocket(), unregisterRequest, true); //we can terminate.nothing is sent back on this terminated socket to cause exception!
		}

		private Thread fetchAvailability(final GetPeersForTorrentMessage peersForTorrent, final PeerDesciptor peer) {
			return new Thread(new Runnable() {
				@Override
				public void run() {
					Socket peerSocketWhoHasMyTorrentAvailable = IOUtil.peerDescriptorToSocket(peer);
					String availabilityRequestString = IOUtil.getInstance(AvailabilityMessage.class).objectToString(new AvailabilityMessage(peersForTorrent.getTorrent()));
					IOUtil.writeMessageToSocket(peerSocketWhoHasMyTorrentAvailable, availabilityRequestString, false);
					String availabilityResponseString = IOUtil.readMessageFromSocket(peerSocketWhoHasMyTorrentAvailable, true);
					AvailabilityMessage availabilityResponseObject = IOUtil.getInstance(AvailabilityMessage.class).stringToObject(availabilityResponseString);
					System.out.println("received: " + availabilityResponseObject + " from peer: " + peer);
					transferQueue.addAvailability(peersForTorrent.getTorrent(), peer, availabilityResponseObject.getAvailableParts());
				}
			});
		}

		private void newfileHandler(AbstractMessage unmarshalledObject, Socket UiSocket) throws IOException {
			System.out.println("Handling NewFileMessageType");
			// Came from the user's UI. needs to pass on a REG message  for the TRACKEr
			NewFileMessage newFileMessage = (NewFileMessage) unmarshalledObject;
			String fileName = newFileMessage.getFileName();
			DataFile dataFile = new DataFile(fileName, IOUtil.createParts(fileName));
			System.out.println("data file created: " + dataFile);
			transferQueue.addSeedingTorrentToQueue(dataFile);
			RegisterMessage registerMessage = new RegisterMessage(dataFile.getTorrent());
			registerMessage.setPeer(responderSocket);
			System.out.println("register message to be sent: " + registerMessage);
			String response = IOUtil.getInstance(RegisterMessage.class).objectToString(registerMessage);
			System.out.println("register message XML to be sent: " + response);
			IOUtil.writeMessageToSocket(getTrackerSocket(), response, false); // announcing  to tracker!
			getTrackerSocket().close();
		}

		private void getpartHandler(AbstractMessage unmarshalledObject, Socket partRequester) {
			GetPartMessage partRequest = (GetPartMessage) unmarshalledObject;
			Part part = partRequest.getPart();
			partRequest.setPart(transferQueue.getIthPartForTorrent(part.getParentTorrent(), part.getIndex()));
			String response = IOUtil.getInstance(GetPartMessage.class).objectToString(partRequest);
			IOUtil.writeMessageToSocket(partRequester, response, true);
		}

		private GetPeersForTorrentMessage peersForTorrentResponseGetter(GetPeersForTorrentMessage peersNeededForTorrent) throws IOException {
			String requestPeersForTorrent = IOUtil.getInstance(GetPeersForTorrentMessage.class).objectToString(peersNeededForTorrent);
			IOUtil.writeMessageToSocket(getTrackerSocket(), requestPeersForTorrent, false);
			String responseFromTrackerWithPeers = IOUtil.readMessageFromSocket(getTrackerSocket(), true); // closing this act (client side) with the tracker  (server side).
			// Recived peers for the torrent of interest from the Tracker
			return IOUtil.getInstance(GetPeersForTorrentMessage.class).stringToObject(responseFromTrackerWithPeers);
		}

		// mediator between the UI client and the tracker.
		private void listOfFilesHandler(AbstractMessage unmarshalledObject, Socket requester) throws UnknownHostException, IOException {
			System.out.println("Handling GetFilesMessage @ peer side");
			GetFilesMessage listOfFiles = (GetFilesMessage) unmarshalledObject;
			// dispatching to the server
			String request = IOUtil.getInstance(GetFilesMessage.class).objectToString(listOfFiles);
			System.out.println("dispatch request string: " + request);
			IOUtil.writeMessageToSocket(getTrackerSocket(), request, false);
			String responseFromTracker = IOUtil.readMessageFromSocket(getTrackerSocket(), true);
			System.out.println("arrived response string: " + responseFromTracker + ", sending Back!");
			
			// delegating response back to the client with the UI.
			IOUtil.writeMessageToSocket(requester, responseFromTracker, true);
		}

		private void availabilityHandler(AbstractMessage unmarshalledObject, Socket requesterPeer) {
			AvailabilityMessage availabilityRequest = (AvailabilityMessage) unmarshalledObject;
			availabilityRequest.setAvailableParts(transferQueue.arrivedPiecesFor(availabilityRequest.getTorrent()));
			System.out.println("responding with availability: " + availabilityRequest);
			String response = IOUtil.getInstance(AvailabilityMessage.class).objectToString(availabilityRequest);
			IOUtil.writeMessageToSocket(requesterPeer, response, true);
		}

		private void getpeersfortorrentHandler(AbstractMessage unmarshalledObject, Socket clientSocket) throws IOException, InterruptedException {
			// client from UI choose a torrent to be downloaded, form the tracker-returned-list.
			GetPeersForTorrentMessage peersNeededForTorrent = (GetPeersForTorrentMessage) unmarshalledObject;
			System.out.println(peersNeededForTorrent + " received @ Peer's Handler");
			Torrent torrentOfInterest = peersNeededForTorrent.getTorrent();
			transferQueue.addLeechingTorrentToQueue(torrentOfInterest);
			while (!transferQueue.isSeeding(torrentOfInterest)) {
				System.out.println("carrying on, as there is still work to do: " + transferQueue.missingPartsFor(torrentOfInterest));
				final GetPeersForTorrentMessage peersForTorrent = peersForTorrentResponseGetter(peersNeededForTorrent);
				System.out.println(peersForTorrent + " tracker returned as peers for this torrent.(" +torrentOfInterest+")" );
				List<Thread> fetchers = new ArrayList<Thread>();
				for (final PeerDesciptor peer : peersForTorrent.getPeers()) {
					Thread availabilityFetchThread = fetchAvailability(peersForTorrent, peer);
					availabilityFetchThread.start(); //paralell fetching
					fetchers.add(availabilityFetchThread);
				}
				Thread.sleep(1000);
				//make sure that each finishes! hopefully by this time some have already
				for (Thread thread : fetchers) {
					thread.join();
				}
				System.out.println("fetched availabilities");
				//Get random {peer and part}
				PeerDesciptor randomPeerForTorrent = transferQueue.getRandomPeerForTorrent(torrentOfInterest, selfDescriptor);
				Part randomPart = transferQueue.getRandomMissingPartFromPeerForTorrent(randomPeerForTorrent, torrentOfInterest);
				while (randomPart == null) {
					randomPeerForTorrent = transferQueue.getRandomPeerForTorrent(torrentOfInterest, selfDescriptor);
					randomPart = transferQueue.getRandomMissingPartFromPeerForTorrent(randomPeerForTorrent, torrentOfInterest);
				}
				if (randomPart.getPartStatus().equals(PartStatus.OK)) {
					//we are good; done, no more missing parts;
					break;
				} else {
					String partRequestString = IOUtil.getInstance(GetPartMessage.class).objectToString(new GetPartMessage(randomPart));
					Socket randomPeerSocket = IOUtil.peerDescriptorToSocket(randomPeerForTorrent);
					IOUtil.writeMessageToSocket(randomPeerSocket, partRequestString, false);
					String getpartResponse = IOUtil.readMessageFromSocket(randomPeerSocket, true);
					GetPartMessage partRespondObject = IOUtil.getInstance(GetPartMessage.class).stringToObject(getpartResponse);
					transferQueue.addPartToTorrent(torrentOfInterest, partRespondObject.getPart());

					if (transferQueue.arrivedPiecesFor(torrentOfInterest).size() == 1) {
						System.out.println("Registering as a new peer for the torrent with tracker");
						//register @ tracker! cause we have at least one part!
						RegisterMessage registerRequestObject = new RegisterMessage(torrentOfInterest);
						registerRequestObject.setPeer(responderSocket);
						String registerRequest = IOUtil.getInstance(RegisterMessage.class).objectToString(registerRequestObject);
						IOUtil.writeMessageToSocket(getTrackerSocket(), registerRequest, true);
					}
				}
			}
			if(transferQueue.isSeeding(torrentOfInterest)){
				System.out.println("Torrent is seeding now!");
				String dataFileString = IOUtil.getInstance(DataFile.class).objectToString(transferQueue.getCompleteDatafile(torrentOfInterest));
				IOUtil.writeMessageToSocket(clientSocket, dataFileString, true);
			} else{
				throw new RuntimeException("torrent should be complete, but its not!");
			}
			
		}
	}

	/**
	 * 13467 and around it shuold be the port!
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		if (args.length != 1) {
			System.exit(-1);
			System.out.println("push in port number!");
		}
		PeerGateway.getInstance(Integer.parseInt(args[0])).acceptConnections();
	}

}
